第四十条 用块引用其所属对象时不要出现保留环

我们使用块的时候,如果不仔细思索,很容易出现“保留环”,我们来举个例子,下面这个类就提供了一套接口,调用者可由此从某个URL中下载数据。在启动获取器时,可设置 completion handler,这个块会在下载结束之后以回调方式执行。为了能在下载完成后通过p_requestCompleted方法执行调用者所指定的块,这段代码需要把completion handler保存到实例变量里面。

// EOCNetwor kFetcher. h
#import <Foundation/Foundation.h>
typedef void(^EOCNetworkFetcherCompletionHandler) (NSData *data);

@interface EOCNetworkFetcher : NSObject 
@property (nonatomic, strong, readonly) NSURL *url;
-(id)initWithURL:(NSURL^)url;
-(void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;



// EOCNetworkFetcher.m
#import "EOCNetworkFetcher.h"
@interface EOCNetworkFetcher ()
@property(nonatomic, strong, readwrite) NSURL *url;
@property(nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler; (nonatomic, strong) NSData *downloadedData;
@implementation EOCNetworkFetcher

-(id)initWithURL:(NSURL*)url {
    if ((self = [super init])) {
        _url = url;
    }
    return sel£;
}

-(void)startWithCompletionHandler:
        (EOCNetworkFetcherCompletionHandlei) completion
{
    self.completionHandler = completion;
    //Start the request
    // Request sets downloadedData property
    //When request is finished, p_requestCompleted is called
}

-(void)p_requestCompleted { 
    if (_completionHandler){ 
    _completionHandler(_downloadedData);
    }
}
@end

某个类可能会创建这种网络数据获取器对象,并用其从URL中下载数据:

@implemantation EOCClass {
    EOCNetworkFetcher *_networkFetcher;
    NSData *_fetchedData;
}
-(void)downloadData {
    NSURL *url = [[NSURL alloc] initWithString:
                    @"http://www.example.com/something.dat"];
    _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^(NSData *data){
        NSLog (@"Request URL %@ finished", _networkFetcher.url);
        _fetchedData = data;
    }];
}
@end

这段代码没有什么问题,但是里面有一个隐蔽的保留环,因为completion handler块要设置_fetchedData实例变量,所以它必须捕获self变量(变量捕获问题详见第37条。这就是说,handler块保留了创建网络数据获取器的那个EOCClass实例。而EOCClass实例则通过strong实例变量保留了获取器,最后,获取器对象又保留了handler块。

如下图所示:

要打破保留环也很容易:要么令_networkFetcher实例变量不再引用获取器,要么令获取器的completionHaiidler属性不再持有handler块。在网络数据获取器这个例子中,应该等 completion handler块执行完毕后,再去打破保留环,以便使获取器对象在handler块执行期间保持存活状态。比方说,completion handler块的代码可以这么修改:

[_networkFetcher startWithCompletionHandler:^(NSData *data){
        NSLog (@"Request URL %@ finished", _networkFetcher.url);
        _fetchedData = data;
        _networkFetcher = nil;
    }];

如果设计API时用到了completion handler这样的同调块,那么很容易形成保留环,所以必须意识到这个重要问题。一般只要适时的清理环中的某个引用即可。

但是上例如果不执行completion handler,那么保留环就无法打破,于是内存就会泄漏。

所以我们使调用API的那段代码无须在执行期间保留指向网络数据获取器的引用,而是设定一套机制,令获取器对象自己设法保持存活。要想保持存活,获取器对象可以 在启动任务时把自己加到全局的collection中(比如用set来实现这个collection),待任务完成后,再移除。

所以我们改写一下:

-(void)downloadData {
    NSURL *url = [[NSURL alloc] initWithString:
                    @"http://www.example.com/something.dat"];
    EOCNetworkFetcher *networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [networkFetcher startWithCompletionHandler:^(NSData *data){
        NSLog (@"Request URL %@ finished", networkFetcher.url);
        _fetchedData = data;
    }];
}

大部分网络通信库都采用这种办法,因为假如令调用者自己来将获取器对象保持存活的话,他们会觉得麻烦。Twitter框架的TWRequest对象也用这个办法。

但是上面这个例子仍然有保留环,completion handler块会通过获取器对象来引用其中的URL,之后获取器会反过来经由CompletionHandler属性保留这个块。我们把块保留在属性里的目的是想稍后来使用这个块。所以一旦我们运行过completion handler之后就可以将它释放了。我们消除保留环可以按照下面修改:

-(void)p_requestCompleted { 
    if (_completionHandler){ 
        _completionHandler(_downloadedData);
    }
    self.completionHandler = nil;
}

这样只要下载请求执行完毕,保留环就解除了。

请注意,之所以要在start方法中把completion handler作为参数传进去,这也是一条重要原因。假如把completion handler暴露为获取器对象的公共属性,那么就不便在执行完下载请求之后直接将其淸理掉了,因为既然已经把handler作为属性公布了,那就意味着调用者可以自由使用它,若是此时又在内部将其清理掉的话,则会破坏“封装语义” (encapsulation semantic)

这两种保留环都很容易发生。使用块来编程时,一不小心就会出现这种bug,反过来说,只要小心谨慎,这种问题也很容易解决。关键在于,要想清楚块可能会捕获并保留哪些对象。如果这些对象又直接或间接保留了块,那么就要考虑怎样在适当的时机解除保留环。

要点

  • 如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。
  • 一定要找个适当的时机解除保留环,而不能把责任推给API的调用者。
2017/9/5 posted in  第六章 块与GCD

第三十九条 用handler块降低代码分散程度

iOS中我们经常采用异步执行任务的方式,来避免主线程的阻塞。因为“系统监控器”(system watchdog)在发现某个应用程序的主线程已经阻塞了一段时间之后,就会令其终止。导致程序崩溃。

但是异步方法执行任务后,需要以某种手段来通知相关代码。实现这一功能有很多方法,常用的技巧是设计一个委托协议,令关注此事件的对象遵从该协议。对象成为delegate之后,就可以在相关事件发生时(例如某个异步任务执行完毕时)得到通知了。例如:

#import <Foundation/Foundation.h>
@class EOCNetworkFetcher;
@protocol EOCNetworkFetcherDelegate <NSObject>
-(void)networkFetcher:(EOCNetworkFetcher*)networkFetcher 
    didFinishWithData:(NSData*)data;
@end

@interface EOCNetworkFetcher : NSObject 
@property (nonatomic, weak) id <EOCNetworkFetcherDelegate> delegate; 
-(id)initWithURL:(NSURL*)url;
-(void)start;
@end

其它类可以像下面这样来使用:

-(void)fetchFooData {
    NSURL *url = [[NSURL alloc] initWithString:
                    @"http: //www.example.com/foo.dat"]; 
    EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url]; 
    fetcher.delegate = self;
    [fetcher start];
}

-(void)networkFetcher:(EOCNetworkFetcher*)networkFetcher didFinishWithData:(NSData*)data
{
    _fetchedFooData = data;
}

上面在EOCNetworkFetcher类中声明了一个协议,协议中有一个方法用于通知对象已获取完数据。

之后想要获取到通知的对象遵守该协议,成为它的委托对象。这样在执行完start方法之后,EOCNetworkFetcher会调用委托对象所遵守的协议方法,让委托对象获取收到的数据(也就是通知它)。

上面这种做法没有错误,确实可行。但是如果我们改用块来写的话,代码会更清晰。就是把completion handler定义为块类型,将其当作参数直接传给start方法:

#import <Foundation/Foundation.h>

typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject 
-(id)initWithURL:(NSURL*)url;
-(void)startWithCompletionHandler:
            (EOCNetworkFetcherCompletionHandler)handler;
@end

这和使用委托协议很想,不过多了个好处,就是可以在调用start方法时直接以内联形式 定义completion handler,以此方式来使用“网络数据获取器”(network fetcher),可以令代码比原先易懂很多。例如,下面这个类就以块的形式来定义completion handler,并以此为参数调用API:

-(void)fetchFooData {
NSURL *url = [[NSURL alloc] initWithString:
@"http://www.example.com/foo.dat"]; 
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data){ 
    _fetchedFooData = data;
});

与使用委托模式的代码相比,用块写出来的代码显然更为整洁。异步任务执行完毕后所需运行的业务逻辑,和启动异步任务所用的代码放在了一起。而且,由于块声明在创建获取器的范围里,所以它可以访问此范围内的全部变量。

这种写法其实最重要的用途是处理错误。现在很多基于块的API都使用块来处理错误,可以分别用两个处理程序来处理操作失败的情况和操作成功的情况。也可以把处理失败情况所需的代码,与处理正常情况所用的代码,都封装到同一个completion handler块里,我们建议使用后者,因为苹果公司也是这样设计API的。我们举例来说:

#import <Foundation/Foundation.h>
@class EOCNetworkFetcher;
typedef void(^EOCNetworkFetcherCompletionHandler) 
                                (NSData *data, NSError *error);

@interface EOCNetworkFetcher : NSObject
-(id)initWithURL:(NSURL*)url;
-(void)startWithCompletionHandler:
                (EOCNetworkFetcherCompletionHandler)completion;
@end

此种API的调用方式如下:

EOCNetworkFetcher *fetcher =
        [[EOCNetworkFetcher alloc] initWithURL:url]; 
[fetcher startWithCompletionHander:
    ^(NSData *data, NSError *error){ 
    if (error) {
        //Handle failure 
    }else {
        // Handle success
    }
}];

要点

  • 在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明。
  • 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切 换,而若改用handler块来实现,则可直接将块与相关对象放在一起。
  • 设计API时如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。
2017/9/4 posted in  第六章 块与GCD

第三十八条 为常用的块类型创建typedef

我们在定义一个块时,语法是这样的:

int^(variableName)(BOOL flag,int value) = ^(BOOL flag, int value){
    //Implemention
    return someInt;
}

此块接受两个类型分别为BOOLint的参数,并返回类型为int的值。并且把它赋给了一个变量。

与其他类型的变量不同,在定义块变量时,要把变量名放在类型之中,而不要放在右侧。这种语法非常难记,也非常难读。鉴于此,我们应该为常用的块类型起个别名,尤其是打算把代码发布成API供他人使用时,更应这样做。开发者可以起个更为易读的名字来表示块的用途,而把块的类型隐藏在其后面。例如:

//定义
typedef int(^EOCSomeBlock)(BOOL flag, int value);

声明变量时,要把名称放在类型中间,并在前面加上“^”符号,而定义新类型时也得这么做。上面这条语句向系统中新增了一个名为EOCSomeBlock的类型。此后,不用再以复杂的块类型来创建变量了,直接使用新类型即可:

EOCSomeBlock block = ^(BOOL flag, int value){
    // Implementation
};

这次代码读起来就顺畅多了:与定义其他变量时一样,变量类型在左边,变量名在右边。

我们可以利用这个将使用块的API做的简单易用些,例如:

-(void)startWithCompletionHandler:
            (void(^)(NSData *data, NSError *error))completion;

上面代码接受了一个块作为参数,所以我们可以用上面定义块的方法来改写它:

typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);

-(void)startWithCompletionHandler: (EOCCompletionHandler)completion;

现在参数看上去就简单多了,而且易于理解。

我们在定义块的时候要注意,最好在使用块类型的类中定义这些typedef,而且还应该把这个类的名字加在由typedef所定义的新类型名前面,这样可以阐明块的用途。还可以用typedef给同一个块签名类型创建数个别名。在这件事上,多多益善。因为,开发者看到类型的别名以及签名中的参数之后,可以很容易的理解类型的用途。

与此相似,如果有好几个类都要执行相似但各有区别的异步任务,而这几个类又不 能放人同一个继承体系,那么,每个类就应该有自己的completion handler类型。这几个completion handler的签名也许完全相同,但最好还是在每个类里都各自定义一个别名,而不 要共用同一个名称。反之,若这些类能纳人同一个继承中,则应该将类型定义语句放在超类中,以供各子类使用。

要点

  • typedef重新定义块类型,可令块变量用起来更加简单。
  • 定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。
  • 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应typedef中的块签名即可,无须改动其他typedef
2017/9/4 posted in  第六章 块与GCD

第三十七条 理解“块”这一概念

首先,块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享同一 个范围内的东西。块用“^”符号来表示,后面跟着一对花括号,括号里面是块的实现代码。 例如,下面就是个简单的块:

^{
    //Block implementation here
}

块其实就是个值,而且自有其相关类型。与intfloatObjective-C对象一样,也可以把块赋给变量,然后像使用其他变量那样使用它。块类型的语法与函数指针近似。下面列出的这个块很简单,没有参数,也不返回值:

void (^someBlock) () = A {
    //Block implementation here
};

这段代码定义了一个名为someBlock的变量。由于变量名写在正中间,所以看上去也许 =有点怪,不过一旦理解了语法,很容易就能读懂。块类型的语法结构如下:

return_type (^block_name)(parameters)

我们来举个例子,下面这种写法所定义的块,返回int值,并且接受两个int做参数:

int (^addBlock) (int a, int b) = ^(int a, int b){
     return a + b;
};

定义好之后,就可以像函数那样使用了。比方说,addBlock块可以这样用:

int add = addBlock (2, 5) ;  //< add = 12

块的强大之处是:在声明它的范围里,所有变量都可以为其所捕获。这也就是说,那个范围里的全部变量,在块里依然可用。比如,下面这段代码所定义的块,就使用了块以外的变量:

int additional = 5;
int (^addBlock) (int a, int b) = ^(int a, int b){ 
    return a + b + additional;
};

int add = addBlock (2, 5);  //< add = 12

默认情况下,为块所捕获的变量,是不可以在块里修改的。在本例中,假如块内的代码改动了additional变量的值,那么编译器就会报错。不过,声明变量的时候可以加上__block 修饰符,这样就可以在块内修改了。

例如:

__block int additional = 5;
int (^addBlock) (int a, int b) = ^(int a, int b){   
    additional++;
    return a + b + additional;
};

int add = addBlock (2, 5);  //< add = 13

块的另一个用法是“内联块”(inline block),例如:

NSArray *array = @[@0, @1, @2, @3, @4, @5];
_block NSInteger count = 0;
[array enumerateObjectsUsingBlock:
    ^(NSNumber *number, NSUInteger idx, BOOL *stop){
        if([number compare:@2] == NSOrderedAscending) { 
        count++;
    }
}];
//count = 2

这段范例代码也演示了“内联块”(inline block)的用法。传给“numerateObjectsUsingBlock:”方法的块并未先賦给局部变量,而是直接内联在函数调用里了。

然后我们在声明和使用块的时候,要注意它的作用范围。定义块的时候,其所占的内存区域是分配在栈中的。这就是说,块只在定义它的那个范围内有效。比如下面这个:

void(^block)();
if ( /* some condition */ ){
    block = ^{
        NSLog(@"Block A");
    };
} else {
    block = ^{
        NSLog(@"Block B");
    };
}
block();

定义在ifelse语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存, 然而等离开了相应的范围之后,编译器有可能把分配给块的内存覆写掉。于是,这两个块只 能保证在对应的ifelse语句范围内有效。这样写出来的代码可以编译,但是运行起来时而正确,时而错误。若编译器未覆写待执行的块,则程序照常运行,若覆写,则程序崩溃。

我们为了解决这个问题可以给块对象发送copy消息以拷贝之。这样的话,就可以把块从栈复制到堆了。

拷贝后的块,可以在定义它的那个范围之外使用。而且,一旦复制到堆上,块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,只是递增块对象的引用计数。如果不再使用这个块,那就应将其释放,在ARC环境下会自动释放。

改动后跟下面一样:

void (^block)();
if (/* some condition */ ){ 
    block = [^{
        NSLog(@,fBlock Aw);
    } copy];
} else {
    block = [^{
        NSLog(@"Block B");
    } copy];
}
block();

除了“桟块”和“堆块”之外,还有一类块叫做“全局块”(global block)。这种块不会捕捉任何状态(比如外围的变量等),运行时也无须有状态来参与。块所使用的整个内存区域,在编译期已经完全确定了,因此,全局块可以声明在全局内存里,而不需要在每次用到 的时候于栈中创建。另外,全局块的拷贝操作是个空操作,因为全局块决不可能为系统所回收。这种块实际上相当于单例。下面就是个全局块:

void (^block)() = ^{
    NSLog(@"This is a block");
};

由于运行该块所需的全部信息都能在编译期确定,所以可把它做成全局块。这完全是种优化技术:若把如此简单的块当成复杂的块来处理,那就会在复制及丢弃该块时执行一些无谓的操作。

要点

  • 块是C、C++、Objective-C中的词法闭包。
  • 块可接受参数,也可返回值。
  • 块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的Objective-C对象一样,具备引用计数了。
2017/9/2 posted in  第六章 块与GCD

通过协议提供匿名方法

协议定义了一系列方法,遵从此协议的对象应该实现它们(如果这些方法不是可选的, 那么就必须实现)。于是,我们可以用协议把自己所写的API之中的实现细节隐藏起来,将返回的对象设计为遵从此协议的纯id类型。

这样的话,想要隐藏的类名就不会出现在API之中了。若是接口背后有多个不同的实现类,而你又不想指明具体使用哪个类,那么可以考虑用这个办法——因为有时候这些类可能会变,有时候它们又无法容纳于标准的类继承体系中,因而不能以某个公共基类来统一表示。

此概念经常称为“匿名对象"(anonymous object),这与其他语言中的“匿名对象”不同,在那些语言中,该词是指以内联形式所创建出来的无名类,而此词在Objective-C中则不是这个意思。

我们之前说的委托与数据源对象,就用到了这个方法。例如在定义“受委托者(delegate)”属性时:

@property {nonatomic, weak) id <EOCDelegate> delegate;

由于该属性的类型是id<EOCDelegate>,所以实际上任何类的对象都能充当这一属性, 即便该类不继承自NSObject也可以,只要遵循EOCDelegate协议就行。对于具备此属性的类来说,delegate就是“匿名的”(ammymous)。如有需要,可在运行期査出此对象所属的类型(参见第14条)。然而这样做不太好,因为指定属性类型时所写的那个EOCDelegate契约已经表明此对象的具体类型无关紧要了。

当然还有其它的用法,这里我们就不举例了。

要点

  • 协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所应实现的方法。
  • 使用匿名对象来隐藏类型名称(或类名)。
  • 如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。
2017/9/2 posted in  第四章 协议与分类

第27条 使用“class-continuation分类”隐藏实现细节

第一种用途

类中经常会包含一些无须对外公布的方法及实例变最。其实这些内容也可以对外公布, 并且写明其为私有,开发者不应依赖它们。但是OC的动态性,使得不可能实现真正的私有方法或私有实例变量。

但是我们最好还是只把确实需要对外公布的那部分内容公开。那么,这种不需对外公布但却应该具有的方法及实例变量应该怎么写呢?此时,这个特殊的“class-continuation分类”就派上用场了。

“class-continuation分类”和普通的分类不同,它必须定义在其所接续的那个类的实现文件里。

例如:

//EOCPerson.m
@interface EOCPerson () {
    NSString * _anInstanceVariable;
}
// Method declarations here 
@end
@implementation EOCPerson {
    int _anotherInstanceVariable;
}
// Method implementations here 
@end

我们这样定义的目的是将这些方法或者实例变量隐藏起来,只供本类使用。即便在公共接口里将其标注为private,也还是会泄漏实现细节。

例如有个绝密的类,不想给其他人知道。 假设你所写的某个类拥有那个绝密类的实例,而这个实例变量又声明在公共接口里面:

#import <Foundation/Foundation.h>

@class EOCSuperSecretClass;

@interface EOCClass : NSObject {
@private
    EOCSuperSecretClass *_secretInstance;
@end

这样别人就会知道有一个叫EOCSuperSecretClass的类了。

所以我们通常应该这样:

// EOCClass.h
#import <Foundation/Foundation.h>
@interface EOCClass : NSObject 
@end

// EOCClass .m 
#import "EOCClass.h"
#import "EOCSuperSecretClass.h"
@interface EOCClass ()  {
    EOCSuperSecretClass *_secretInstance;
@end

@implementation EOCClass
// Methods here
@end

第二种用途

编写Objective-C++代码时 “class-continuation分类”也很有用。Objective-C++OCC++的混合体,其代码可以用这两种语言来编写。由于兼容性原因,游戏后 端一般用C++来写。另外,有时候要使用的第三方库可能只有C++绑定,此时也必须使用 C++来编码。在这些情况下,使用"class-continuation分类"会很方便。假设某个类打算这样写:

#import <Foundation/Foundation.h> 
#include "SomeCppClass.h" 
@interface EOCClass : NSObject { 
@private
    SomeCppClass _cppClass;
@end

该类的实现文件可能叫做EOCClass.mm,其中.mm扩展名表示编译器应该将此文件按Objective-C++来编译,否则,就无法正确引人SomeCppClass.h了。然而请注意,名为SomeCppClass的这个C++类必须完全引入,因为编译器要完整地解析其定义方能得知_cppClass实例变量的大小。于是,只要是包含EOCClass.h的类,都必须编译为 Objective-C++才行,因为它们都引入了SomeCppClass类的头文件。这很快就会失控,最终 导致整个应用程序全部都要编译为ObjeCtive-C++。这样显然会增加编码的负担。

也许我们会想用前向声明来避免导入SomeCppClass.h,比如:

#import <Foundation/Foundation.h> 

class SomeCppClass;

@interface EOCClass : NSObject { 
@private
    SomeCppClass *_cppClass;
@end

现在实例变量必须是指针,若不是,则编译器无法得知其大小,从而会报错。但所有指针的大小确实都是固定的,于是编译器只需知道其所指的类型即可。

虽然我们这样做没有#include "SomeCppClass.h"但是我们前向声明该类时所用的class关键字还是C++下的关键字,所以仍然需要按照OC来编译才行。

我们这里的解决方法还是一样,既然变量是private的,我们还是可以将它在“class-continuation分类”声明,改写成:

// EOCClass. h

#import <Foundation/Foundation.h>

@interface EOCClass : NSObject


// EOCClass.mm 
#import "EOCClass.h"
#include "SomeCppClass.h"

@interface EOCClass ()  {
    SomeCppClass _cppClass;
}
@end

@implementation EOCClass 
@end

改写后的EOCClass类,其头文件里就没有C++代码了,使用头文件的人甚至意识不到其底层实现代码中混有C++成分。某些系统库用到了这种模式,比如网页浏览器框架WebKit,其大部分代码都以C++编写,然而对外展示出来的却是一套整洁的Objective-C接口。CoreAnimation里面也用到了此模式,它的许多后端代码都用C++写成,但对外公布的却是一套纯Objective-C接口。

第三种用法

就是将public接口中声明为“只读”的 属性扩展为“可读写”,以便在类的内部设置其值。

例如:

// .h文件
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName; 
@property (nonatomic, copy, readonly) NSString *lastName;

-(id) initWithFirstName : (NSString*) firstName
                lastName: (NSString*) lastName;

@end

我们一般会在“class-continuaticm分类”中把这两个属性扩展为“可读写”:

@interface EOCPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;

-(void)p_privateMethod;
@end

只需要用上面几行代码就行了。现在EOCPerson的实现代码可以随意调用“setFirstName:”“setLastName:”这两个设置方法,也可以用“点语法”来设置属性。这样做很有用,既能令外界无法修改对象,又能在其内部按照需要管理其数据。

只会在类的实现代码中用到的私有方法也可以声明在“class-continuation分类”中。这么做比较合适,因为它描述了那些只在类实现代码中才会使用的方法。上述的私有方法加上了p_前缀。

第四种用法

当我们想要把对象所遵守的协议视为私有,就可以在“class-continuation分类”中声明。例如:

#import "EOCPerson•h"
#import "EOCSecretDelegate.h"
@interface EOCPerson () <EOCSecretDelegate> 
@end
@implementation EOCPerson
    /*.....*/
@end

要点

  • 通过“class-continuation分类”向类中新增实例变量。

  • 如果某属性在主接口中声明为“只读”,而类的内部又要用设置方法修改此属性,那么就在“class-continuation分类”中将其扩展为“可读写”

  • 把私有方法的原型声明在“class-continuation分类”里面。

  • 若想使类所遵循的协议不为人所知,则可于“class-continuation分类”中声明。

2017/9/1 posted in  第四章 协议与分类